import numbers
from enum import Enum, unique
from pathlib import Path
from typing import Any, TypeAlias

import numpy as np
import pandas as pd

from ..dataclass.gdimData import (
    GdimJsonTable,
    GdimMinIOFile,
    GdimTemplate,
    SimpleJsonTable,
)
from ..dataclass.geoProfiles import (
    BoreForCadDraw,
    BoreForPlanDraw,
    MaterialTable,
    MultiProfile1D,
    Profile1D,
    Secction3D,
    SectionForCadDraw,
)
from ..dataclass.geoStructures import BasicCompositePile, BasicPile, RecFoundation
from ..dataclass.results import ComplexResult, CoordinateSystem, DocData, SingleResult
from ..dataclass.tables import TableCollection, TableData, TableSeries
from .pipeData import MultiResultsDict as MultiResultsDictModel
from .pipeData import ResultsDict as ResultsDictModel


class PortTypeHint:
    SingleValue: TypeAlias = numbers.Number | str
    Number: TypeAlias = numbers.Number
    NumberArray: TypeAlias = np.ndarray | pd.Series  # A 1d array of numbers
    GeneralArray: TypeAlias = (
        np.ndarray | pd.Series | list[Any]
    )  # A 1d array of general values
    FilePath: TypeAlias = Path | str
    FilesPath: TypeAlias = list[Path | str]
    NumberTable: TypeAlias = pd.DataFrame
    GeneralTable: TypeAlias = pd.DataFrame
    HttpUrl: TypeAlias = str
    Token: TypeAlias = tuple[
        str | None, int | str | None, str | None
    ]  # (token, proj_id, host)
    TableData: TypeAlias = TableData
    TableSeries: TypeAlias = TableSeries
    TableCollection: TypeAlias = TableCollection
    SingleResult: TypeAlias = SingleResult
    ComplexResult: TypeAlias = ComplexResult
    TableRowMask: TypeAlias = np.ndarray  # A mask for a row, 1D array of boolean values
    TablesRowMask: TypeAlias = dict[str, np.ndarray]  # key is the table name
    Attributes: TypeAlias = dict[str, Any]
    JsonObject: TypeAlias = dict  # A general json data object
    DocData: TypeAlias = DocData  # A container for data to be written to a file
    General: TypeAlias = Any  # A port to input or output any value
    # Type hints for Pydantic models (avoiding circular import)
    ResultsDict: TypeAlias = ResultsDictModel
    MultiResultsDict: TypeAlias = MultiResultsDictModel
    CoordinateSystem: TypeAlias = CoordinateSystem
    SingleResultList: TypeAlias = list[SingleResult]
    TableDataList: TypeAlias = list[TableData]
    TableCollectionList: TypeAlias = list[TableCollection]

    #################
    # Geo port types
    #################
    Profile1D: TypeAlias = Profile1D
    MultiProfile1D: TypeAlias = MultiProfile1D
    MaterialTable: TypeAlias = MaterialTable
    MaterialTableCollection: TypeAlias = dict[str, MaterialTable]
    BoreForCadDraw: TypeAlias = BoreForCadDraw
    BoreForPlanDraw: TypeAlias = BoreForPlanDraw
    RecFoundation: TypeAlias = RecFoundation
    Pile: TypeAlias = BasicPile
    CompositePile: TypeAlias = BasicCompositePile
    GeoSection: TypeAlias = SectionForCadDraw
    GeoSection3D: TypeAlias = Secction3D
    GeoSections: TypeAlias = list[SectionForCadDraw]
    GeoSections3D: TypeAlias = list[Secction3D]

    #################
    # Gdim port types
    #################
    GdimTemplate: TypeAlias = GdimTemplate
    GdimJsonTable: TypeAlias = GdimJsonTable
    SimpleJsonTables: TypeAlias = list[SimpleJsonTable]
    GdimFile: TypeAlias = GdimMinIOFile
    GdimFiles: TypeAlias = list[GdimMinIOFile]

    #################
    # legacy port types
    #################
    GdimTemplateOld: TypeAlias = dict[str, dict[str, Any]]


@unique
class PortType(int, Enum):
    SingleValue = 1  # A single value - number or string
    Number = 2  # A single number
    NumberArray = 3  # A 1d array of numbers
    GeneralArray = 4  # A 1d array of general values
    FilePath = 5  # FilePath
    FilesPath = 6  # A list of FilePath
    NumberTable = 7  # A 2d table of numbers, DataFrame
    GeneralTable = 8  # A 2d table of general values, DataFrame
    HttpUrl = 9  # A http or https url
    Token = 10  # A token
    TableData = 11  # A table data with meta data
    TableSeries = 12  # A table series with meta data
    TableCollection = 13  # A table collection with meta data
    SingleResult = 14  # A dict container for calculation result
    ComplexResult = 15  # A container for complext calculation result
    TableRowMask = 16  # A mask for a table row
    TablesRowMask = 17  # A dict container for table row masks, key is the table name, value is the mask for the table row
    Attributes = 18  # A dict container for attributes
    JsonObject = 19  # A general json data object
    DocData = 20  # A dict container for data to be written to a .docx document
    General = 21  # A general value
    ResultsDict = 22  # A dict to store all kinds of results
    MultiResultsDict = 23  # A dict to store all kinds of results
    CoordinateSystem = 24  # A coordinate system
    SingleResultList = 25  # A list of SingleResult
    TableDataList = 26  # A list of TableData
    TableCollectionList = 27  # A list of TableCollection

    #################
    # Geo port types
    #################
    Profile1D = 30  # A 1d geo profile
    MultiProfile1D = 31  # Multi 1d geo profiles
    MaterialTable = 32  # A geo material table
    MaterialTableCollection = 33  # A dict container for material tables, key is the material table name, value is the material table
    BoreForCadDraw = 34  # A bore data for CAD drawing
    RecFoundation = 35  # A rectangular foundation
    Pile = 36  # A pile
    CompositePile = 37  # A composite pile
    BoreForPlanDraw = 38  # A bore data for plan drawing
    GeoSection = 39  # A geo section
    GeoSection3D = 40  # A geo section 3d
    GeoSections = 41  # A list of geo sections
    GeoSections3D = 42  # A list of geo sections 3d

    #################
    # Gdim port types
    #################
    GdimTemplate = 50  # A json data structure for GdimTemplate
    GdimJsonTable = 51  # A json data structure for Table to render on Gdim
    SimpleJsonTables = 52  # A simple json data structure for Tables to render on Gdim
    GdimFile = 53  # A file object of Gdim
    GdimFiles = 54  # A list of file objects of Gdim

    #################
    # legacy port types
    #################
    GdimTemplateOld = 60


def get_port_type_hint_to_port_type_mapping() -> dict[Any, PortType]:
    """Get direct mapping from PortTypeHint class attributes to PortType enums.

    This provides an exact 1:1 mapping and is the preferred way to map
    PortTypeHint types to PortType enums.
    """
    return {
        PortTypeHint.SingleValue: PortType.SingleValue,
        PortTypeHint.Number: PortType.Number,
        PortTypeHint.NumberArray: PortType.NumberArray,
        PortTypeHint.GeneralArray: PortType.GeneralArray,
        PortTypeHint.FilePath: PortType.FilePath,
        PortTypeHint.FilesPath: PortType.FilesPath,
        PortTypeHint.NumberTable: PortType.NumberTable,
        PortTypeHint.GeneralTable: PortType.GeneralTable,
        PortTypeHint.HttpUrl: PortType.HttpUrl,
        PortTypeHint.Token: PortType.Token,
        PortTypeHint.TableData: PortType.TableData,
        PortTypeHint.TableSeries: PortType.TableSeries,
        PortTypeHint.TableCollection: PortType.TableCollection,
        PortTypeHint.SingleResult: PortType.SingleResult,
        PortTypeHint.ComplexResult: PortType.ComplexResult,
        PortTypeHint.TableRowMask: PortType.TableRowMask,
        PortTypeHint.TablesRowMask: PortType.TablesRowMask,
        PortTypeHint.Attributes: PortType.Attributes,
        PortTypeHint.JsonObject: PortType.JsonObject,
        PortTypeHint.DocData: PortType.DocData,
        PortTypeHint.General: PortType.General,
        PortTypeHint.ResultsDict: PortType.ResultsDict,
        PortTypeHint.MultiResultsDict: PortType.MultiResultsDict,
        PortTypeHint.CoordinateSystem: PortType.CoordinateSystem,
        PortTypeHint.SingleResultList: PortType.SingleResultList,
        PortTypeHint.TableDataList: PortType.TableDataList,
        PortTypeHint.TableCollectionList: PortType.TableCollectionList,
        # Geo types
        PortTypeHint.Profile1D: PortType.Profile1D,
        PortTypeHint.MultiProfile1D: PortType.MultiProfile1D,
        PortTypeHint.MaterialTable: PortType.MaterialTable,
        PortTypeHint.MaterialTableCollection: PortType.MaterialTableCollection,
        PortTypeHint.BoreForCadDraw: PortType.BoreForCadDraw,
        PortTypeHint.RecFoundation: PortType.RecFoundation,
        PortTypeHint.Pile: PortType.Pile,
        PortTypeHint.CompositePile: PortType.CompositePile,
        PortTypeHint.BoreForPlanDraw: PortType.BoreForPlanDraw,
        # Gdim types
        PortTypeHint.GdimTemplate: PortType.GdimTemplate,
        PortTypeHint.GdimJsonTable: PortType.GdimJsonTable,
        PortTypeHint.SimpleJsonTables: PortType.SimpleJsonTables,
        PortTypeHint.GdimFile: PortType.GdimFile,
        PortTypeHint.GdimFiles: PortType.GdimFiles,
    }


def infer_port_types_from_union_args(union_args) -> list[PortType]:
    """Infer PortTypes from union arguments using name-based mapping.

    This function uses the naming convention between PortTypeHint and PortType:
    PortTypeHint.TableData -> PortType.TableData
    PortTypeHint.NumberArray -> PortType.NumberArray
    etc.
    """
    # Create name-to-PortType mapping for fast lookup
    name_to_port_type = {port_type.name: port_type for port_type in PortType}

    # Get all PortTypeHint attribute names and their values for identification
    port_type_hint_attrs = {}
    for attr_name in dir(PortTypeHint):
        if not attr_name.startswith("_"):
            attr_value = getattr(PortTypeHint, attr_name)
            port_type_hint_attrs[attr_value] = attr_name

    matched_port_types = set()
    used_union_args = set()

    # 1. First pass: Try to reconstruct expanded union types (higher priority)
    # Only decompose actual Union types, not generic types like list[Something]
    union_attrs_with_components = []
    for attr_name in dir(PortTypeHint):
        if not attr_name.startswith("_") and attr_name in name_to_port_type:
            attr_value = getattr(PortTypeHint, attr_name)
            if hasattr(attr_value, "__args__"):
                from typing import Union, get_args, get_origin

                # Only decompose actual Union types (e.g., Path | str), not generic types
                origin = get_origin(attr_value)

                # Check if this is a union type (either typing.Union or types.UnionType)
                is_union_type = False
                if origin is Union:
                    is_union_type = True
                elif hasattr(attr_value, "__class__") and "UnionType" in str(
                    attr_value.__class__
                ):
                    is_union_type = True

                if is_union_type:
                    component_types = set(get_args(attr_value))
                    # Only include if it has multiple components (actual union)
                    if len(component_types) > 1:
                        union_attrs_with_components.append(
                            (attr_name, attr_value, component_types)
                        )

    # Sort by number of components (descending) to prioritize more specific unions
    union_attrs_with_components.sort(key=lambda x: len(x[2]), reverse=True)

    remaining_args = set(union_args)

    for attr_name, attr_value, component_types in union_attrs_with_components:
        # If all components are in remaining args, this is likely the original attribute
        if component_types.issubset(remaining_args):
            matched_port_types.add(name_to_port_type[attr_name])
            # Remove the matched components to avoid conflicts
            remaining_args -= component_types

    # 2. Second pass: Match remaining direct PortTypeHint attributes
    for union_arg in remaining_args.copy():
        if union_arg in port_type_hint_attrs:
            attr_name = port_type_hint_attrs[union_arg]
            if attr_name in name_to_port_type:
                matched_port_types.add(name_to_port_type[attr_name])
                remaining_args.remove(union_arg)

    return list(matched_port_types)
